<?php
//session_start(); // 开启session功能
/*
 * LogAdminLite
 * 开源作品-PHP在线通用日志浏览器(单文件绿色版)
 * 程序特性：
 *      1. 单文件绿色版,部署超简单
 *      2. 支持目录树形层级结构显示，支持目录展开或收起
 *      3. 支持日志文件下载、文件删除、文件查看、目录清空功能
 *      4. 支持用户登录验证、IP验证等多种权限验证功能
 * 使用方法：
 *      1. 复制本文件到web任意目录
 *      2. 修改$authconfig配置信息为实际内容
 *      3. 在浏览器输入链接路径,开始使用
 * 说明：写这个程序主要是为了辅助项目开发中的日志文件查看,提供了程序特性描述的功能(基本能满足项目开发需要). 如果您有兴趣一起完善这个小程序,欢迎使用git一起开发~感谢！
 * 项目周期：
 *      2017-8-16 立项开发
 *      2017-8-16 程序完成
 * 版权：不限商业用途! 如有修改代码请保留原著署名,并把修改后的代码回馈到作者邮箱：<14507247@qq.com>,促进本产品进步.做一个有公德心的正能量程序员~
 *      作者博客：http://www.cnblogs.com/sochishun/ 欢迎交流~
 * @version 1.0 2017-8-16 SoChishun <14507247@qq.com> Added.
 * @version 1.1 2017-8-28 SoChishun 新增子目录内容视图功能，跳转限定在子目录中；新增清空目录功能
 * @version 1.2 2017-10-13 SoChishun 重构代码格式，纯PHP代码改为HTML混合代码，英文按钮改为中文按钮
 * @version 1.3 2017-11-1 SoChishun $ioutil->tail方法改为直接输出,解决大文件读取显示空白的问题
 * @version 1.4 2017-11-3 SoChishun 为适应负载均衡多台服务器分布式使用场景,登录会话保持由单机的session存储改为url参数token
 * @version 1.5 2017-11-28 SoChishun 改进token功能，增加续期功能，避免频繁超时重新登录的繁琐操作
 * @version 1.6 2017-12-19 SoChishun 登录方法封装成JWTLite类,配置信息处理方法封装成ServerConfUtil类,方便管理和迁移
 * @version 1.7 2018-1-11 SoChishun 增加侧边栏排序功能
 * @version 1.8 2018-1-12 SoChishun 引入ptshead.php头文件,简化代码,提高代码复用; 文件操作由函数重构为FileUtil类,方便其他小工具代码复用; 新增配置数据,可配置要忽略的目录
 */

// 版本号
define('VERSION', '1.8.0');
// 程序名称
define('APPNAME', 'LogAdminLite');
// 日志根目录
define('ROOTPATH', '../logs/');

// 引入头文件
include './pts_head.php';
// #JWTLite配置位置
// 页面访问验证配置
JWTLite::check_login();
list($loginid, $token, $loginexp) = array_values(JWTLite::$LOGIN_RESULT);

// 主要配置
$config = array(
    /* 要忽略的路径, 注意,目录必需以斜杠结尾 */
    'skip' => array('../.git/', '../logs/2018-01-10/'),
);

// 获取输入参数
$path = input('path'); // 路径
$dirpath = input('dirpath'); // 目录路径
$action = input('action'); // 操作
if (input('debugmsg')) {
    debugmsg('logadminlite.php[0]', 'debugtest');
}

if ($path) {
    if (!file_exists($path)) {
        exit('路径 [' . $path . '] 不存在! <a href="?path' . ROOTPATH . '&token=' . $token . '">[返回]</a>');
    }
    if ($action == "delete" || $action == "clear") {
        try {
            if (is_dir($path)) {
                $ioutil->rmdir($path, 'delete' == $action);
            } else {
                unlink($path);
            }
        } catch (Exception $ex) {
            echo $ex->getMessage();
        }
        echo 'delete' == $action ? '删除成功!' : '清空成功!';
        if ($dirpath) {
            header('location:?path=' . $dirpath . '&dirpath=' . $dirpath . '&token=' . $token);
        } else {
            header('location:?path=' . ROOTPATH . '&token=' . $token);
        }
        exit;
    }
}
?>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title><?php echo ($path ? short_text(basename($path), 16, true) . ' - ' : ''), APPNAME, ' - ', VERSION ?></title>
        <style type="text/css">
            body{font-size:13px;}
            a{text-decoration:none}
            h1 a { color:#555; font-weight: normal;}
            .aside{width:240px;max-height:600px;overflow:auto;float:left;font-size:13px}
            .main{width:800px;float:left;margin-left:10px;}
            ul{margin:0}li{list-style:none;margin-left:-40px;}
            li.active { color:#F00;}
            li.active a{ color:#F00;}
            text{color:#333;}
        </style>
    </head>
    <body>
        <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
        <?php
        echo '<h1>', APPNAME, ' ';
        echo '<a href="?token=', $token, '" title="新开一个页面" target="_blank" style="font-size:12px;">[新窗口]</a>';
        echo '<a href="#" title="刷新当前页面" style="font-size:12px;" onclick="location.reload()">[刷新]</a>';
        echo '</h1>';
        // 用户登录信息
        if (isset($loginid)) {
            echo '<span title="登录过期时间：', $loginexp, ' (超时前进行跳转操作能让登录会话自动延期,避免重登录操作~)">', $loginid, '</span>';
            echo ' [<a href="?action=logout" onclick="return confirm(\'您确定要注销登录吗?\');">注销登录</a>]';
        }
        echo ' 客户端IP:', $_SERVER['REMOTE_ADDR'], ' <span style="display:none">服务器IP:' . JWTLite::get_host_ip() . '</span> <a href="#" id="lnk-toggle-ipinfo">[...]</a>';

        echo '<div><a href="?path=', ROOTPATH, '&token=', $token, '">[根目录]</a></div>';
        if ($path) {
            if ($dirpath) { // 限定在子目录中
                echo '<div style="margin:3px 0; color:#666">当前路径：', $dirpath;
                echo ' [<a title="refresh,刷新" href="?path=' . $dirpath . '&dirpath=', $dirpath, '&token=', $token, '">刷新</a>]';
                echo '[<a title="clear,清空目录" href="?path=' . $dirpath . '&dirpath=' . $dirpath . '&token=', $token, '&action=clear" onclick="return confirm(\'您确定要清空当前目录吗?\')">清空</a>]';
                echo '[<a title="delete,删除目录" href="?path=' . $dirpath . '&dirpath=' . $dirpath . '&token=', $token, '&action=delete" onclick="return confirm(\'您确定要删除当前目录吗?\')">删除</a>]';
                echo '</div>';
            } else { // 限定在根目录中
                echo '<div style="margin:3px 0; color:#666">当前路径：', $path;
                echo ' [<a title="refresh,刷新" href="?path=' . $path . '&token=', $token, '">刷新</a>]';
                if (is_file($path)) {
                    echo '[<a title="download [右键点击下载]" href="' . $path . '&token=', $token, '">&darr;</a>]';
                }
                if (is_dir($path)) {
                    echo '[<a title="clear,清空目录" href="?path=' . $path . '&dirpath=' . $path . '&token=', $token, '&action=clear" onclick="return confirm(\'您确定要清空当前目录吗?\')">清空</a>]';
                    echo '[<a title="delete,删除目录" href="?path=' . $path . '&dirpath=' . $path . '&token=', $token, '&action=delete" onclick="return confirm(\'您确定要删除当前目录吗?\')">删除</a>]';
                }
                echo '</div>';
            }
        }
        ?>
        <div class="aside">
            <?php
            echo '<ul>';
            if ($dirpath) {
                $files = FileUtil::scandirReturn($dirpath, 0, true, $config['skip']);
                //fn_scandir_echo($dirpath, 0, true, $path, $dirpath, $token);
            } else {
                $files = FileUtil::scandirReturn(($path && is_dir($path) ? $path : ROOTPATH), 0, true, $config['skip']);
                //fn_scandir_echo(($path && is_dir($path) ? $path : ROOTPATH), 0, true, $path, $dirpath, $token);
            }
            FileUtil::showFiles($token, $files, 0, $path, $dirpath);
            echo '</ul>';
            ?>
        </div>
        <div class="main">
            <?php
            if ($path && is_file($path)) {
                echo '<div style="color:#666">size:', FileUtil::formatBytes(filesize($path)), ', modified time:', date('Y-m-d H:i:s', filectime($path)), ', readable:', is_readable($path), ', writeable:', is_writable($path), ', executable:', is_executable($path), '</div>';
                echo '<textarea cols="150" rows="40">';
                FileUtil::tail($path);
                echo '</textarea>';
            }
            ?>
        </div>
        <div style="clear:both"></div>
        <script>
            // 2017-10-28 服务器信息改为显隐控制，提高安全性，防止被窥探
            $('#lnk-toggle-ipinfo').click(function () {
                var $a = $(this);
                if ($a.text() == '[...]') {
                    $a.text('[x]').prev().show();
                } else {
                    $a.text('[...]').prev().hide();
                }
                return false;
            })
            $(".btn_switch").click(function () {
                var $this = $(this);
                if ($this.text() == "[-]") {
                    $this.text("[+]").attr("title", "展开");
                    $this.parent().children("ul").hide();
                } else {
                    $this.text("[-]").attr("title", "收起");
                    $this.parent().children("ul").show();
                }
            });
        </script>
    </body>
</html>
<?php

/**
 * 调试日志
 * @param string $location 产生消息的文件位置
 * @param mixed $msg 消息数据
 * @param boolean|array $enableOrOption 是否可用,用于临时关闭单个调试信息 ([enable,to_json,msg_type=debug|show])
 * @param boolean $to_josn 是否以json格式输出
 * @param string $msg_type 调试消息类型(error,info,debug,show,show表示立即输出,并保存到日志文件)
 * @version 2017-8-14 add
 * @version 2017-11-28 新增type参数
 * @version 2017-12-14 $enable参数改为$enableOrOption,支持数组格式的控制参数
 */
function debugmsg($location, $msg, $enableOrOption = true, $to_josn = false, $msg_type = 'debug') {
    // 参数判断
    if (is_array($enableOrOption)) {
        $defaultOption = array(
            'enable' => true, // 是否可用,用于临时关闭单个调试信息
            'to_json' => false, // 是否以json格式输出
            'msg_type' => 'debug', // error,info,debug,show,show表示立即输出,并保存到日志文件
        );
        $defaultOption = array_merge($defaultOption, $enableOrOption);
        list($enable, $to_josn, $msg_type) = array_values($defaultOption);
    } else {
        $enable = $enableOrOption;
    }
    // 白名单和黑名单
    if (!$enable) {
        // 白名单
        $whites = array();
        if ($whites) {
            foreach ($whites as $white) {
                if (false !== strpos($location, $white)) {
                    $enable = true;
                    break;
                }
            }
        }
    } else {
        // 黑名单
        $blacks = array('bm/index.action.php', 'xiacai.action.php', 'rank.action.php', 'user.php', 'setting.action.php', 'user.action.php', 'app.action.php', 'gamesModel.class.php');
        if ($blacks) {
            foreach ($blacks as $black) {
                if (false !== strpos($location, $black)) {
                    $enable = false;
                    false;
                }
            }
        }
    }
    if (!$enable) {
        return; // 关闭调试
    }
    // 消息内容格式化
    if (!is_string($msg)) {
        if (is_array($msg) && $to_josn) {
            $msg = json_encode($msg);
        } else {
            $msg = var_export($msg, true);
        }
    }
    // 立即输出
    if ($msg_type == 'show') {
        echo date('Y-m-d H:i:s'), ' ', $location, '<br />', PHP_EOL, $msg, '<hr />', PHP_EOL;
    }
    // 位置独立日志文件配置
    $standaloneLocations = array('ylcpsapi.action.php' => 'ylcpsapi', 'ylsoaapi.action.php' => 'ylsoaapi', 'video.action.php' => 'video');
    // 文件名
    $filename = 'log';
    foreach ($standaloneLocations as $standaloneLocation => $aliasFile) {
        if (false !== strpos($location, $standaloneLocation)) {
            $filename = $aliasFile;
        }
    }
    // 类型独立日志文件配置,在文件名后面加消息类型后缀,如:log-error-20171129.txt
    $standaloneTypes = array('error');
    if ($msg_type && in_array($msg_type, $standaloneTypes)) {
        $filename .= '-' . $msg_type;
    }
    $content = date('Y-m-d H:i:s') . PHP_EOL . $location . PHP_EOL . $msg . PHP_EOL . PHP_EOL;
    $dir = APP_ROOT_PATH . 'java/logs/debug/';
    if (!is_dir($dir)) {
        mkdir($dir);
    }
    $path = $dir . $filename . '-' . date('mdH') . '.txt';
    if (file_exists($path) && (filesize($path) > 2097152)) { // 超过2MB自动分文件
        $path = $dir . $filename . '-' . date('mdHi') . '.txt';
    }
    // 写入日志到文件
    if ($fp = fopen($filepath, 'a')) {
        $startTime = microtime();
// 对文件进行加锁时，设置一个超时时间为1ms，如果这里时间内没有获得锁，就反复获得，直接获得到对文件操作权为止，当然。如果超时限制已到，就必需马上退出，让出锁让其它进程来进行操作。
        do {
            $canWrite = flock($fp, LOCK_EX);
            if (!$canWrite) {
                usleep(round(rand(0, 100) * 1000));
            }
        } while ((!$canWrite) && ((microtime() - $startTime) < 1000));
        if ($canWrite) {
            fwrite($fp, $content);
        }
        fclose($fp);
    }
    //file_put_contents($path, $content, FILE_APPEND);
}
